// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Mono.Cecil;

namespace Mono.Linker
{
    internal sealed partial class DocumentationSignatureGenerator
    {
        /// <summary>
        ///  A visitor that generates the part of the documentation comment after the initial type
        ///  and colon.
        ///  Adapted from Roslyn's DocumentattionCommentIDVisitor.PartVisitor:
        ///  https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/DocumentationComments/DocumentationCommentIDVisitor.PartVisitor.cs
        /// </summary>
        internal sealed class PartVisitor
        {
            internal static readonly PartVisitor Instance = new PartVisitor();

            private PartVisitor()
            {
            }

            public void VisitArrayType(ArrayType arrayType, StringBuilder builder, ITryResolveMetadata resolver)
            {
                VisitTypeReference(arrayType.ElementType, builder, resolver);

                // Rank-one arrays are displayed different than rectangular arrays
                if (arrayType.IsVector)
                {
                    builder.Append("[]");
                }
                else
                {
                    // C# arrays only support zero lower bounds
                    if (arrayType.Dimensions[0].LowerBound != 0)
                        throw new NotImplementedException();
                    builder.Append("[0:");
                    for (int i = 1; i < arrayType.Rank; i++)
                    {
                        if (arrayType.Dimensions[0].LowerBound != 0)
                            throw new NotImplementedException();
                        builder.Append(",0:");
                    }

                    builder.Append(']');
                }
            }

            public void VisitField(FieldDefinition field, StringBuilder builder, ITryResolveMetadata resolver)
            {
                VisitTypeReference(field.DeclaringType, builder, resolver);
                builder.Append('.').Append(field.Name);
            }

            private void VisitParameters(IEnumerable<ParameterDefinition> parameters, bool isVararg, StringBuilder builder, ITryResolveMetadata resolver)
            {
                builder.Append('(');
                bool needsComma = false;

                foreach (var parameter in parameters)
                {
                    if (needsComma)
                        builder.Append(',');

                    // byrefs are tracked on the parameter type, not the parameter,
                    // so we don't have VisitParameter that Roslyn uses.
                    VisitTypeReference(parameter.ParameterType, builder, resolver);
                    needsComma = true;
                }

                // note: the C# doc comment generator outputs an extra comma for varargs
                // methods that also have fixed parameters
                if (isVararg && needsComma)
                    builder.Append(',');

                builder.Append(')');
            }

            public void VisitMethodDefinition(MethodDefinition method, StringBuilder builder, ITryResolveMetadata resolver)
            {
                VisitTypeReference(method.DeclaringType, builder, resolver);
                builder.Append('.').Append(GetEscapedMetadataName(method));

                if (method.HasGenericParameters)
                    builder.Append("``").Append(method.GenericParameters.Count);

                if (method.HasMetadataParameters() || (method.CallingConvention == MethodCallingConvention.VarArg))
#pragma warning disable RS0030 // MethodReference.Parameters is banned. This generates documentation signatures, so it's okay to use it here
                    VisitParameters(method.Parameters, method.CallingConvention == MethodCallingConvention.VarArg, builder, resolver);
#pragma warning restore RS0030

                if (method.Name == "op_Implicit" || method.Name == "op_Explicit")
                {
                    builder.Append('~');
                    VisitTypeReference(method.ReturnType, builder, resolver);
                }
            }

            public void VisitProperty(PropertyDefinition property, StringBuilder builder, ITryResolveMetadata resolver)
            {
                VisitTypeReference(property.DeclaringType, builder, resolver);
                builder.Append('.').Append(GetEscapedMetadataName(property));

                if (property.Parameters.Count > 0)
                    VisitParameters(property.Parameters, false, builder, resolver);
            }

            public void VisitEvent(EventDefinition evt, StringBuilder builder, ITryResolveMetadata resolver)
            {
                VisitTypeReference(evt.DeclaringType, builder, resolver);
                builder.Append('.').Append(GetEscapedMetadataName(evt));
            }

            public static void VisitGenericParameter(GenericParameter genericParameter, StringBuilder builder)
            {
                Debug.Assert(genericParameter.DeclaringMethod == null ^ genericParameter.DeclaringType == null);
                // Is this a type parameter on a type?
                if (genericParameter.DeclaringMethod != null)
                {
                    builder.Append("``");
                }
                else
                {
                    Debug.Assert(genericParameter.DeclaringType != null);

                    // If the containing type is nested within other types.
                    // e.g. A<T>.B<U>.M<V>(T t, U u, V v) should be M(`0, `1, ``0).
                    // Roslyn needs to add generic arities of parents, but the innermost type redeclares
                    // all generic parameters so we don't need to add them.
                    builder.Append('`');
                }

                builder.Append(genericParameter.Position);
            }

            public void VisitTypeReference(TypeReference typeReference, StringBuilder builder, ITryResolveMetadata resolver)
            {
                switch (typeReference)
                {
                    case ByReferenceType byReferenceType:
                        VisitByReferenceType(byReferenceType, builder, resolver);
                        return;
                    case PointerType pointerType:
                        VisitPointerType(pointerType, builder, resolver);
                        return;
                    case ArrayType arrayType:
                        VisitArrayType(arrayType, builder, resolver);
                        return;
                    case GenericParameter genericParameter:
                        VisitGenericParameter(genericParameter, builder);
                        return;
                }

                if (typeReference.IsNested)
                {
                    Debug.Assert(typeReference is not SentinelType && typeReference is not PinnedType);
                    // GetInflatedDeclaringType may return null for generic parameters, byrefs, and pointers, but these
                    // are separately handled above.
                    VisitTypeReference(typeReference.GetInflatedDeclaringType(resolver)!, builder, resolver);
                    builder.Append('.');
                }

                if (!string.IsNullOrEmpty(typeReference.Namespace))
                    builder.Append(typeReference.Namespace).Append('.');

                // This includes '`n' for mangled generic types
                builder.Append(typeReference.Name);

                // For uninstantiated generic types (we already built the mangled name)
                // or non-generic types, we are done.
                if (typeReference.HasGenericParameters || typeReference is not GenericInstanceType genericInstance)
                    return;

                // Compute arity counting only the newly-introduced generic parameters
                var declaringType = genericInstance.DeclaringType;
                var declaringArity = 0;
                if (declaringType != null && declaringType.HasGenericParameters)
                    declaringArity = declaringType.GenericParameters.Count;
                var totalArity = genericInstance.GenericArguments.Count;
                var arity = totalArity - declaringArity;

                // Un-mangle the generic type name
                var suffixLength = arity.ToString().Length + 1;
                builder.Remove(builder.Length - suffixLength, suffixLength);

                // Append type arguments excluding arguments for re-declared parent generic parameters
                builder.Append('{');
                bool needsComma = false;
                for (int i = totalArity - arity; i < totalArity; ++i)
                {
                    if (needsComma)
                        builder.Append(',');
                    var typeArgument = genericInstance.GenericArguments[i];
                    VisitTypeReference(typeArgument, builder, resolver);
                    needsComma = true;
                }
                builder.Append('}');
            }

            public void VisitPointerType(PointerType pointerType, StringBuilder builder, ITryResolveMetadata resolver)
            {
                VisitTypeReference(pointerType.ElementType, builder, resolver);
                builder.Append('*');
            }

            public void VisitByReferenceType(ByReferenceType byReferenceType, StringBuilder builder, ITryResolveMetadata resolver)
            {
                VisitTypeReference(byReferenceType.ElementType, builder, resolver);
                builder.Append('@');
            }

            private static string GetEscapedMetadataName(IMemberDefinition member)
            {
                var name = member.Name.Replace('.', '#');
                // Not sure if the following replacements are necessary, but
                // they are included to match Roslyn.
                return name.Replace('<', '{').Replace('>', '}');
            }
        }
    }
}
